import os
import typing as t
import contextvars
import threading
import socketio

import click
import flask
import flask_restx
import openstack
from asgiref.wsgi import WsgiToAsgi
from click import Command
from elasticsearch import Elasticsearch
from flask import Flask, current_app, jsonify, request
from flask.ctx import RequestContext
from flask_cors import CORS
from flask_migrate import Migrate
from flask_sqlalchemy import get_debug_queries

from configs.settings import celery_config, flask_config
from core import ExtApix, ExtBlueprint, ExtRule, connect
from core.log import save_log

from core.middleware import ReplayBlocker


class App(Flask):

    url_rule_class = ExtRule

    # 当前request ctx缓存
    current_request_cache = contextvars.ContextVar('current_request_cache', default=None)

    # 全局中间件注册
    MIDDLEWARE: t.Tuple[t.Callable] = (ReplayBlocker,)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.init_config()
        if not self.config["DEBUG"]:
            self.format_error_response()

        self.es_conn = None  # elasticsearch connection

        # 请勿轻易打乱顺序
        self.monkey_patch()  # 重载组件
        self.register_extensions()  # 其他组件注册初始化

        # 当app在cli中初始化时，跳过该部分
        if not os.environ.get("FLASK_RUN_FROM_CLI"):
            self.after_init_app()  # app初始化后操作

        # 中间件挂载
        self.mount_middleware()

        # 挂载日志记录
        self.after_request_funcs.setdefault(None, []).append(save_log)

    def init_config(self):
        """初始化配置"""
        self.config.from_object(flask_config)

    def format_error_response(self):
        """返回通用错误响应"""

        @self.errorhandler(400)
        def bad_request(e):
            response = jsonify(message="Bad Request")
            response.status_code = 400
            return response

        @self.errorhandler(401)
        def reject(e):
            response = jsonify(message="Forbidden")
            response.status_code = 403
            return response

        @self.errorhandler(403)
        def forbidden(e):
            response = jsonify(message="Forbidden")
            response.status_code = 403
            return response

        @self.errorhandler(404)
        def page_not_found(e):
            response = jsonify(message="Not Found")
            response.status_code = 404
            return response

        @self.errorhandler(500)
        def internal_server_error(e):
            response = jsonify(message="Internal Server Error")
            response.status_code = 500
            return response

    @staticmethod
    def monkey_patch():
        """重载依赖"""
        # 重载openstack连接对象
        openstack.connect = connect

        # 重载蓝图及Api适配权限
        flask.Blueprint = ExtBlueprint

        flask_restx.api.Api = flask_restx.Api = ExtApix

    def register_extensions(self):
        """注册扩展"""
        from configs.log_config import LogConfig
        from tools import cache, celery_app, db, redis_conn

        db.init_app(self)
        cache.init_app(self)
        redis_conn.init_app(self)
        celery_app.init_app(self, celery_config)

        Migrate(app=self, db=db)
        LogConfig(self)
        CORS(self, resources={r"/*": {"origins": "*"}})

        # self.register_elasticsearch()
        self.register_blueprints()  # 注册蓝图

    def register_blueprints(self):
        """注册蓝图"""
        from app.api import (
            AdminBluPrint,
            authBlueprint,
            collectorBlueprint,
            ctfCompetitionDrill,
            ctfQuestionManagement,
            homeIndex,
            imageBlueprint,
            loadBlueprint,
            loadBlueprint_par,
            permissionBlueprint,
            rangeSceneSimulation,
            restoreFactorySettings,
            safetyKnowledge,
            studentAthleticsCompetition,
            studentCommon,
            subjectManagement,
            userManagement,
            WebhookBluPrint
        )

        self.register_blueprint(imageBlueprint)
        self.register_blueprint(authBlueprint)
        self.register_blueprint(AdminBluPrint)
        self.register_blueprint(collectorBlueprint)
        self.register_blueprint(homeIndex)
        self.register_blueprint(userManagement)
        self.register_blueprint(rangeSceneSimulation)
        self.register_blueprint(ctfQuestionManagement)
        self.register_blueprint(safetyKnowledge)
        self.register_blueprint(ctfCompetitionDrill)
        self.register_blueprint(restoreFactorySettings)
        self.register_blueprint(loadBlueprint)
        self.register_blueprint(loadBlueprint_par)
        self.register_blueprint(studentAthleticsCompetition)
        self.register_blueprint(subjectManagement)
        self.register_blueprint(studentCommon)
        self.register_blueprint(permissionBlueprint)
        self.register_blueprint(WebhookBluPrint)

    def register_elasticsearch(self):
        """注册elasticsearch"""
        self.es_conn = (
            Elasticsearch([self.config["ELASTICSEARCH_URL"]])
            if self.config.get("ELASTICSEARCH_URL", None)
            else None
        )

    def after_init_app(self):
        """项目启动前置操作可以放在这里"""
        ...

    def add_manager_command(self, name, func, *args, option_prefix="--", **kwargs):
        """注册管理命令简单方法, 无需多个装饰器，复杂命令请仍旧使用click.command装饰器
        def example_command(name, title=None):
            print("example_command", name, title)

        app.add_manager_command("example_command", example_command, "name", title="Example Command")
        """

        for attr in args:
            func = click.argument(attr)(func)
        for op in kwargs:
            if option_prefix is not None and not op.startswith(option_prefix):
                op = f"{option_prefix}{op}"
            func = click.option(op)(func)

        f = click.command(name)(func)
        self.cli.add_command(f)

    def mount_middleware(self):
        """挂载中间件"""
        for item in self.MIDDLEWARE:
            self.wsgi_app = item(app=self, wsgi_handler=self.wsgi_app)

    def request_context(self, environ: dict) -> RequestContext:
        """重载以支持对请求数据的重复读取"""
        _ctx = self.current_request_cache.get()
        if _ctx is None:
            self.current_request_cache.set(
                super().request_context(environ)
            )
        return self.current_request_cache.get()


def create_app():

    app = App(__name__)

    from . import command

    # from utils.key_manager import KeyManager
    #
    # if not KeyManager().check_key():
    #     exit(4)
    # 将自定义命令注册到flask组中
    for model in command.__dict__.values():
        if isinstance(model, Command):
            app.cli.add_command(model)
            
    from tools.startup_network import network_init
    from tools.apscheduler_tools import load_scheduled_jobs
    @app.before_first_request
    def do_something():
        # 在项目启动后执行的方法
        network_init()
        scheduler = load_scheduled_jobs()
        scheduler.start()
        
    @app.route("/ping", role_control=False)
    def ping():
        return {"code": 200, "data": "pong!"}

    app.logger.warning("\n\n!!!!! 任何蓝图、路由变化，请执行`flask init_permission`重新初始化权限!!!\n")

    @app.before_request
    def middleware():
        # 虚拟机及靶场
        vm_endpoint = [
            "api.vmenter",
            "api.vmoperate",
            "api.vmmanage",
            "api.vmenv",
            "api.rangetopology",
        ]
        curr = str(request.endpoint)
        from utils.utils import checkCourseRange, checkCtfQuestion, checkCtfRangeStatus

        if curr in vm_endpoint and request.method in ["POST", "PUT", "DELETE"]:
            # 判断此靶场id有关联课程就不能修改
            ...
        if curr == "api.ctfquestion" and request.method in ["PUT", "DELETE"]:
            ctf_question_id = request.view_args["id"]
            # ctf与试题建立了关系无法删除
            checkCtfQuestion(ctf_question_id)

        if curr == "api.coursewaremanage" and request.method in [
            "PUT",
            "DELETE",
        ]:
            courseware_id = (request.path.split("/"))[-1]
            checkCourseRange(courseware_id)

        # 学生启动环境时
        if curr == "part.startenvvm":
            range_uuid = request.get_json()["range_uuid"]
            checkCtfRangeStatus(range_uuid)
        # 克隆的场景用户自己启动元件，检查是否存在二层网，不存在则创建，
        network_endpoint = [
            "range_scene_list.switch_network",
            "range_scene_list.vm_operate",
            "range_scene_list.router"
        ]
        if curr in network_endpoint and request.method  == "POST":
            from app.models import rangeModel, switchModel
            from tools.basic_tools import Tools
            from tools import db
            
            args_data = request.get_json()
            range_uuid = args_data["range_uuid"]
            range_info = rangeModel.RangeLinkModule.query.filter_by(range_uuid=range_uuid).first()
            if not range_info.network_id:
                network_id = Tools.get_uuid4_no_line()
                range_obj = rangeModel.Range.query.get(range_info.range_id)
                # 创建并更新数据库
                with db.auto_commit():
                    rangeModel.RangeLinkModule.query.filter_by(
                        range_uuid=range_uuid
                    ).update({"network_id": network_id})
                    
                from app.api.v1.webhook.webhook import WebHookManager
                import requests, time
                load_cloud_object = WebHookManager()                
                result= load_cloud_object.create_l2_vxlan_network(
                        range_info.vxlan_pool_id,
                        range_obj.name,
                        current_app.config["ZSTACK_CLOUD"]["zone_uuid"],
                        network_id
                    )
                localtion = result["location"]
                response = requests.get(url=localtion)
                index = 0
                while index < 5:
                    response = requests.get(url=localtion)
                    if response.status_code == 200:
                        break
                    time.sleep(1)
                    index += 1
                
        return

    if app.config["DEBUG"]:

        @app.after_request
        def after_request(response):
            for query in get_debug_queries():
                if query.duration >= current_app.config["SLOW_DB_QUERY_TIME"]:
                    current_app.logger.warning(
                        "Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n"
                        % (
                            query.statement,
                            query.parameters,
                            query.duration,
                            query.context,
                        )
                    )
            return response

    return app


def create_asgi_app(app: Flask = None):
    _app = app if app else create_app()
    return WsgiToAsgi(_app)


def create_socketio_app(asgi_app: Flask = None):
    _app = asgi_app if asgi_app else create_asgi_app()

    mgr = socketio.AsyncRedisManager(_app.wsgi_application.config["REDIS_URL"])
    sio = socketio.AsyncServer(
        client_manager=mgr,
        async_mode="asgi",
        logger=True,
        engineio_logger=True,
        cors_allowed_origins="*",
        ping_interval=60 * 60 * 12
    )
    return socketio.ASGIApp(sio, other_asgi_app=_app)

class SioApp:

    __sio_app = contextvars.ContextVar("sio_app", default=None)
    __lock = threading.Lock()

    def __new__(cls, *args, app=None, **kwargs) -> socketio.ASGIApp:
        with cls.__lock:
            if cls.__sio_app.get() is not None:
                return cls.__sio_app.get()
            cls.__sio_app.set(create_socketio_app(app or create_asgi_app()))
            return cls.__sio_app.get()

